package com.easipass.framework.eirframeworkcoredsl;

import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.List;

public abstract class AbstractTokenizer {

    private static final byte[] flags = new byte[256];

    private static final byte IS_DIGIT = 0x01;

    private static final byte IS_HEXDIGIT = 0x02;

    private static final byte IS_ALPHA = 0x04;

    static {
        for (int ch = '0'; ch <= '9'; ch++) {
            flags[ch] |= IS_DIGIT | IS_HEXDIGIT;
        }
        for (int ch = 'A'; ch <= 'F'; ch++) {
            flags[ch] |= IS_HEXDIGIT;
        }
        for (int ch = 'a'; ch <= 'f'; ch++) {
            flags[ch] |= IS_HEXDIGIT;
        }
        for (int ch = 'A'; ch <= 'Z'; ch++) {
            flags[ch] |= IS_ALPHA;
        }
        for (int ch = 'a'; ch <= 'z'; ch++) {
            flags[ch] |= IS_ALPHA;
        }
    }

    private final int[] NO_LINEBREAKS = new int[0];

    protected String expressionString;

    protected char[] toProcess;

    protected int max;

    protected int pos;

    protected List<Token> tokens = new ArrayList<Token>();

    protected int[] linebreaks = NO_LINEBREAKS;

    abstract void process();

    public Tokens getTokens(String inputData) {
        this.expressionString = inputData;
        this.toProcess = (inputData + "\0").toCharArray();
        this.max = toProcess.length;
        this.pos = 0;
        this.tokens.clear();
        process();
        return new Tokens(inputData, tokens, linebreaks);
    }

    protected boolean isTwoCharToken(TokenKind kind) {
        Assert.isTrue(kind.tokenChars.length == 2, "The token kind being looked for should be of length 2");
        Assert.isTrue(toProcess[pos] == kind.tokenChars[0],
                "Expected these characters to have already been tested for equality");
        return toProcess[pos + 1] == kind.tokenChars[1];
    }

    protected boolean isCharToken(TokenKind kind) {
        Assert.isTrue(kind.tokenChars.length == 1, "The token kind being looked for should be of length 1");
        return toProcess[pos] == kind.tokenChars[0] && toProcess[pos + 1] != kind.tokenChars[0];
    }

    protected void pushCharToken(TokenKind kind) {
        tokens.add(new Token(kind, pos, pos + 1));
        pos++;
    }

    protected void pushPairToken(TokenKind kind) {
        tokens.add(new Token(kind, pos, pos + 2));
        pos += 2;
    }

    protected boolean isIdentifier(char ch) {
        return isAlphabetic(ch) || isDigit(ch) || ch == '_' || ch == '$';
    }

    protected boolean isQuote(char ch) {
        return ch == '\'' || ch == '"';
    }

    protected boolean isWhitespace(char ch) {
        return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
    }

    protected boolean isDigit(char ch) {
        if (ch > 255) {
            return false;
        }
        return (flags[ch] & IS_DIGIT) != 0;
    }

    protected boolean isAlphabetic(char ch) {
        if (ch > 255) {
            return false;
        }
        return (flags[ch] & IS_ALPHA) != 0;
    }

    protected char[] subArray(int start, int end) {
        char[] result = new char[end - start];
        System.arraycopy(toProcess, start, result, 0, end - start);
        return result;
    }

    protected void lexIdentifier() {
        int start = pos;
        do {
            pos++;
        }
        while (isIdentifier(toProcess[pos]));
        char[] subarray = subArray(start, pos);
        tokens.add(new Token(TokenKind.IDENTIFIER, subarray, start, pos));
    }

    protected void lexQuotedStringLiteral() {
        lexStringLiteral('\'', DSLMessage.NON_TERMINATING_QUOTED_STRING);
    }

    protected void lexDoubleQuotedStringLiteral() {
        lexStringLiteral('"', DSLMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING);
    }

    private void lexStringLiteral(char quoteChar, DSLMessage messageOnNonTerminationError) {
        int start = pos;
        boolean terminated = false;
        while (!terminated) {
            pos++;
            char ch = toProcess[pos];
            if (ch == quoteChar) {
                if (toProcess[pos + 1] == quoteChar) {
                    pos++;
                } else {
                    terminated = true;
                }
            }
            if (ch == 0) {
                throw new ParseException(expressionString, start, messageOnNonTerminationError);
            }
        }
        pos++;
        tokens.add(new Token(TokenKind.LITERAL_STRING, subArray(start, pos), start, pos));
    }

    protected boolean isArgValueIdentifierTerminator(char ch, boolean quoteOpen) {
        return (ch == '|' && !quoteOpen) || (ch == ';' && !quoteOpen) || ch == '\0' || (ch == ' ' && !quoteOpen)
                || (ch == '\t' && !quoteOpen) || (ch == '>' && !quoteOpen) || (ch == '\r' && !quoteOpen)
                || (ch == '\n' && !quoteOpen);
    }

    protected void lexArgValueIdentifier() {
        int start = pos;
        boolean quoteOpen = false;
        int quoteClosedCount = 0;
        Character quoteInUse = null;
        if (isQuote(toProcess[pos])) {
            quoteOpen = true;
            quoteInUse = toProcess[pos++];
        }
        do {
            char ch = toProcess[pos];
            if ((quoteInUse != null && ch == quoteInUse) || (quoteInUse == null && isQuote(ch))) {
                if (quoteInUse != null && quoteInUse == '\'' && ch == '\'' && toProcess[pos + 1] == '\'') {
                    pos++;
                } else {
                    quoteOpen = !quoteOpen;
                    if (!quoteOpen) {
                        quoteClosedCount++;
                    }
                }
            }
            pos++;
        }
        while (!isArgValueIdentifierTerminator(toProcess[pos], quoteOpen));
        char[] subarray = null;
        if (quoteInUse != null && quoteInUse == '"' && quoteClosedCount == 0) {
            throw new ParseException(expressionString, start, DSLMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING);
        } else if (quoteInUse != null && quoteInUse == '\'' && quoteClosedCount == 0) {
            throw new ParseException(expressionString, start, DSLMessage.NON_TERMINATING_QUOTED_STRING);
        } else if (quoteClosedCount == 1 && sameQuotes(start, pos - 1)) {
            tokens.add(new Token(TokenKind.LITERAL_STRING, subArray(start, pos), start, pos));
        } else {
            subarray = subArray(start, pos);
            tokens.add(new Token(TokenKind.IDENTIFIER, subarray, start, pos));
        }
    }

    protected boolean sameQuotes(int pos1, int pos2) {
        if (toProcess[pos1] == '\'') {
            return toProcess[pos2] == '\'';
        } else if (toProcess[pos1] == '"') {
            return toProcess[pos2] == '"';
        }
        return false;
    }

    @Override
    public String toString() {
        StringBuilder s = new StringBuilder();
        s.append(this.expressionString).append("\n");
        for (int i = 0; i < this.pos; i++) {
            s.append(" ");
        }
        s.append("^\n");
        s.append(tokens).append("\n");
        return s.toString();
    }

    public void raiseException(DSLMessage message, Object... inserts) {
        throw new ParseException(expressionString, pos, message, inserts);
    }

    protected void addLinebreak() {
        int[] newLinebreaks = new int[linebreaks.length + 1];
        System.arraycopy(linebreaks, 0, newLinebreaks, 0, linebreaks.length);
        newLinebreaks[linebreaks.length] = pos;
        linebreaks = newLinebreaks;
        pos++;
    }
}
